import { findDiffStart, findDiffEnd } from "./diff";

// ::- A fragment represents a node's collection of child nodes.
// 一个片段代表一个节点的子节点集合
//
// Like nodes, fragments are persistent data structures, and you
// should not mutate them or their content. Rather, you create new
// instances whenever needed. The API tries to make this easy.

/**
 和节点一样，片段也是持久的数据结构，而且你
 不应改变它们或其内容。而是在需要时随时创建新的实例。API试图让这一切变得简单。
 */
export class Fragment {
    constructor(content, size) {
        this.content = content;
        // console.log(this.content);
        // :: number
        // The size of the fragment, which is the total of the size of its
        // content nodes.
        // 该片段的大小，是其内容节点大小的总和。
        this.size = size || 0;
        // console.log(this.content, this.size);
        if (size == null)
            for (let i = 0; i < content.length; i++)
                this.size += content[i].nodeSize;
    }

    // :: (number, number, (node: Node, start: number, parent: Node, index: number) → ?bool, ?number)
    // Invoke a callback for all descendant nodes between the given two
    // positions (relative to start of this fragment). Doesn't descend
    // into a node when the callback returns `false`.
    /**
        对给定的两个位置之间的所有下行节点（相对于这个片段的开始）调用一个回调。
        当回调返回 "false "时，不会下降到一个节点。
     */
    nodesBetween(from, to, f, nodeStart = 0, parent) {
        // console.log(from, to);
        for (let i = 0, pos = 0; pos < to; i++) {
            let child = this.content[i],
                end = pos + child.nodeSize;
            if (
                end > from &&
                f(child, nodeStart + pos, parent, i) !== false &&
                child.content.size
            ) {
                let start = pos + 1;
                child.nodesBetween(
                    Math.max(0, from - start),
                    Math.min(child.content.size, to - start),
                    f,
                    nodeStart + start
                );
            }
            pos = end;
        }
    }

    // :: ((node: Node, pos: number, parent: Node) → ?bool)
    // Call the given callback for every descendant node. The callback
    // may return `false` to prevent traversal of a given node's children.
    /**
     对每个子孙节点调用给定的回调。回调可能返回 "false"，以防止遍历给定节点的子节点。
     */
    descendants(f) {
        this.nodesBetween(0, this.size, f);
    }

    // : (number, number, ?string, ?string) → string
    textBetween(from, to, blockSeparator, leafText) {
        let text = "",
            separated = true;
        this.nodesBetween(
            from,
            to,
            (node, pos) => {
                if (node.isText) {
                    text += node.text.slice(
                        Math.max(from, pos) - pos,
                        to - pos
                    );
                    separated = !blockSeparator;
                } else if (node.isLeaf && leafText) {
                    text += leafText;
                    separated = !blockSeparator;
                } else if (!separated && node.isBlock) {
                    text += blockSeparator;
                    separated = true;
                }
            },
            0
        );
        return text;
    }

    // :: (Fragment) → Fragment
    // Create a new fragment containing the combined content of this
    // fragment and the other.
    append(other) {
        if (!other.size) return this;
        if (!this.size) return other;
        let last = this.lastChild,
            first = other.firstChild,
            content = this.content.slice(),
            i = 0;
        if (last.isText && last.sameMarkup(first)) {
            content[content.length - 1] = last.withText(last.text + first.text);
            i = 1;
        }
        for (; i < other.content.length; i++) content.push(other.content[i]);
        return new Fragment(content, this.size + other.size);
    }

    // :: (number, ?number) → Fragment
    // Cut out the sub-fragment between the two given positions.
    cut(from, to) {
        if (to == null) to = this.size;
        if (from == 0 && to == this.size) return this;
        let result = [],
            size = 0;
        if (to > from)
            for (let i = 0, pos = 0; pos < to; i++) {
                let child = this.content[i],
                    end = pos + child.nodeSize;
                if (end > from) {
                    if (pos < from || end > to) {
                        if (child.isText)
                            child = child.cut(
                                Math.max(0, from - pos),
                                Math.min(child.text.length, to - pos)
                            );
                        else
                            child = child.cut(
                                Math.max(0, from - pos - 1),
                                Math.min(child.content.size, to - pos - 1)
                            );
                    }
                    result.push(child);
                    size += child.nodeSize;
                }
                pos = end;
            }
        return new Fragment(result, size);
    }

    cutByIndex(from, to) {
        if (from == to) return Fragment.empty;
        if (from == 0 && to == this.content.length) return this;
        return new Fragment(this.content.slice(from, to));
    }

    // :: (number, Node) → Fragment
    // Create a new fragment in which the node at the given index is
    // replaced by the given node.
    replaceChild(index, node) {
        let current = this.content[index];
        if (current == node) return this;
        let copy = this.content.slice();
        let size = this.size + node.nodeSize - current.nodeSize;
        copy[index] = node;
        return new Fragment(copy, size);
    }

    // : (Node) → Fragment
    // Create a new fragment by prepending the given node to this
    // fragment.
    addToStart(node) {
        return new Fragment(
            [node].concat(this.content),
            this.size + node.nodeSize
        );
    }

    // : (Node) → Fragment
    // Create a new fragment by appending the given node to this
    // fragment.
    addToEnd(node) {
        return new Fragment(
            this.content.concat(node),
            this.size + node.nodeSize
        );
    }

    // :: (Fragment) → bool
    // Compare this fragment to another one.
    eq(other) {
        if (this.content.length != other.content.length) return false;
        for (let i = 0; i < this.content.length; i++)
            if (!this.content[i].eq(other.content[i])) return false;
        return true;
    }

    // :: ?Node
    // The first child of the fragment, or `null` if it is empty.
    get firstChild() {
        return this.content.length ? this.content[0] : null;
    }

    // :: ?Node
    // The last child of the fragment, or `null` if it is empty.
    get lastChild() {
        return this.content.length
            ? this.content[this.content.length - 1]
            : null;
    }

    // :: number
    // The number of child nodes in this fragment.
    get childCount() {
        return this.content.length;
    }

    // :: (number) → Node
    // Get the child node at the given index. Raise an error when the
    // index is out of range.
    child(index) {
        let found = this.content[index];
        if (!found)
            throw new RangeError(
                "Index " + index + " out of range for " + this
            );
        return found;
    }

    // :: (number) → ?Node
    // Get the child node at the given index, if it exists.
    maybeChild(index) {
        return this.content[index];
    }

    // :: ((node: Node, offset: number, index: number))
    // Call `f` for every child node, passing the node, its offset
    // into this parent node, and its index.
    forEach(f) {
        for (let i = 0, p = 0; i < this.content.length; i++) {
            let child = this.content[i];
            f(child, p, i);
            p += child.nodeSize;
        }
    }

    // :: (Fragment) → ?number
    // Find the first position at which this fragment and another
    // fragment differ, or `null` if they are the same.
    findDiffStart(other, pos = 0) {
        return findDiffStart(this, other, pos);
    }

    // :: (Fragment) → ?{a: number, b: number}
    // Find the first position, searching from the end, at which this
    // fragment and the given fragment differ, or `null` if they are the
    // same. Since this position will not be the same in both nodes, an
    // object with two separate positions is returned.
    findDiffEnd(other, pos = this.size, otherPos = other.size) {
        return findDiffEnd(this, other, pos, otherPos);
    }

    // : (number, ?number) → {index: number, offset: number}
    // Find the index and inner offset corresponding to a given relative
    // position in this fragment. The result object will be reused
    // (overwritten) the next time the function is called. (Not public.)
    findIndex(pos, round = -1) {
        if (pos == 0) return retIndex(0, pos);
        if (pos == this.size) return retIndex(this.content.length, pos);
        if (pos > this.size || pos < 0)
            throw new RangeError(
                `Position ${pos} outside of fragment (${this})`
            );
        for (let i = 0, curPos = 0; ; i++) {
            let cur = this.child(i),
                end = curPos + cur.nodeSize;
            if (end >= pos) {
                if (end == pos || round > 0) return retIndex(i + 1, end);
                return retIndex(i, curPos);
            }
            curPos = end;
        }
    }

    // :: () → string
    // Return a debugging string that describes this fragment.
    toString() {
        return "<" + this.toStringInner() + ">";
    }

    toStringInner() {
        return this.content.join(", ");
    }

    // :: () → ?Object
    // Create a JSON-serializeable representation of this fragment.
    toJSON() {
        return this.content.length ? this.content.map((n) => n.toJSON()) : null;
    }

    // :: (Schema, ?Object) → Fragment
    // Deserialize a fragment from its JSON representation.
    static fromJSON(schema, value) {
        if (!value) return Fragment.empty;
        if (!Array.isArray(value))
            throw new RangeError("Invalid input for Fragment.fromJSON");
        return new Fragment(value.map(schema.nodeFromJSON));
    }

    // :: ([Node]) → Fragment
    // Build a fragment from an array of nodes. Ensures that adjacent
    // text nodes with the same marks are joined together.
    static fromArray(array) {
        if (!array.length) return Fragment.empty;
        let joined,
            size = 0;
        for (let i = 0; i < array.length; i++) {
            let node = array[i];
            size += node.nodeSize;
            if (i && node.isText && array[i - 1].sameMarkup(node)) {
                if (!joined) joined = array.slice(0, i);
                joined[joined.length - 1] = node.withText(
                    joined[joined.length - 1].text + node.text
                );
            } else if (joined) {
                joined.push(node);
            }
        }
        return new Fragment(joined || array, size);
    }

    // :: (?union<Fragment, Node, [Node]>) → Fragment
    // Create a fragment from something that can be interpreted as a set
    // of nodes. For `null`, it returns the empty fragment. For a
    // fragment, the fragment itself. For a node or array of nodes, a
    // fragment containing those nodes.
    static from(nodes) {
        if (!nodes) return Fragment.empty;
        if (nodes instanceof Fragment) return nodes;
        if (Array.isArray(nodes)) return this.fromArray(nodes);
        if (nodes.attrs) return new Fragment([nodes], nodes.nodeSize);
        throw new RangeError(
            "Can not convert " +
            nodes +
            " to a Fragment" +
            (nodes.nodesBetween
                ? " (looks like multiple versions of prosemirror-model were loaded)"
                : "")
        );
    }
}

const found = { index: 0, offset: 0 };
function retIndex(index, offset) {
    found.index = index;
    found.offset = offset;
    return found;
}

// :: Fragment
// An empty fragment. Intended to be reused whenever a node doesn't
// contain anything (rather than allocating a new empty fragment for
// each leaf node).
Fragment.empty = new Fragment([], 0);
